package edu.hawaii.halealohacli.kmj;

import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import javax.xml.datatype.XMLGregorianCalendar;
import org.wattdepot.client.WattDepotClient;
import org.wattdepot.resource.source.jaxb.Source;
import org.wattdepot.util.tstamp.Tstamp;

/**
 * Stores baseline power-use information given a date for which to collect baseline data. This is a
 * non-Command class used by the Command set-baseline [tower | lounge] [date].
 * 
 * @author Jordan Takayama
 */
public class Baseline {
  /**
   * The hours in a day.
   */
  private static final int HOURS_IN_DAY = 24;

  /**
   * Formats energy consumption values to two decimal places.
   */
  private final DecimalFormat VALUE_FORMAT = new DecimalFormat("###.00");

  /**
   * Formats time values to a format that can be used to generate XMLGregorianCalendars.
   */
  private final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("'T'HH:mm:ss.SSS",
      Locale.getDefault());

  /**
   * Formats date values to conform to the format used in XMLGregorianCalendars.
   */
  private final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd",
      Locale.getDefault());

  /**
   * An array of doubles representing energy consumption values in kWh.
   */
  private double[] baselineValues = new double[HOURS_IN_DAY];

  /**
   * The date for which baseline data was taken, in yyyy-MM-dd format.
   */
  private String baselineDate;
  
  /**
   * The name of the tower or lounge for which baseline data was taken.
   */
  private String sourceName;

  /**
   * Retrieves the baseline energy consumption values in kWh.
   * 
   * @return The array of baseline energy consumption values in kWh.
   */
  public double[] getBaselineValues() {
    return baselineValues.clone();
  }

  /**
   * Sets the baseline energy consumption values to new values.
   * 
   * @param baselineValues The new array of baseline energy consumption values in kWh.
   */
  public void setBaselineValues(double[] baselineValues) {
    this.baselineValues = baselineValues.clone();
  }

  /**
   * Retrieves the date, in yyyy-MM-dd format, for which baseline data was taken.
   * 
   * @return The date for which baseline data was taken, in yyyy-MM-dd format.
   */
  public String getBaselineDate() {
    return baselineDate;
  }

  /**
   * Sets the date, in yyyy-MM-dd format, for which baseline data was taken.
   * 
   * @param baselineDate The date for which baseline data was taken, in yyyy-MM-dd format.
   */
  public void setBaselineDate(String baselineDate) {
    this.baselineDate = baselineDate;
  }
  
  /**
   * Retrieves the name of the tower or lounge for which baseline data was taken.
   * 
   * @return The name of the tower or lounge for which baseline data was taken.
   */
  public String getSourceName() {
    return sourceName;
  }

  /**
   * Sets the name of the tower or lounge for which baseline data was taken.
   * 
   * @param sourceName The name of the tower or lounge for which baseline data was taken.
   */
  public void setSourceName(String sourceName) {
    this.sourceName = sourceName;
  }

  /**
   * A default constructor for testing purposes. It is used when a test only needs to access the
   * methods of this class (e.g., setters or utility methods) without needing to access actual
   * energy consumption data.
   */
  protected Baseline() {
    // Default constructor.
  }

  /**
   * Constructor for an object which stores baseline power-use information.
   * 
   * @param client The WattDepotClient used to retrieve energy consumption data.
   * @param source The WattDepot source used to retrieve energy consumption data.
   * @param date The date used to make the baseline data requests. This method does not perform date
   * range checking.
   * @throws Exception If the Source passed to this constructor is null.
   */
  public Baseline(WattDepotClient client, Source source, String date) throws Exception {
    // Time-related variables.
    Date today = new Date();
    String todayString = DATE_FORMAT.format(today);
    String yesterday =
        generateYesterday(Calendar.getInstance(TimeZone.getDefault(), Locale.getDefault()));
    // Calendars used for data requests.
    Calendar start;
    Calendar currentRequestTime;
    double[] energyConsumption = new double[HOURS_IN_DAY];
    // The date defaults to yesterday.
    if (date == null || date.equals(todayString)) {
      this.baselineDate = yesterday;
      if ((date != null) && date.equals(todayString)) {
        System.out.format(
            "\nCannot use null or the current day as a baseline, defaulting to %s.\n", yesterday);
      }
    }
    else {
      if (source == null) {
        throw new Exception("\nFailed to generate baseline from a null source.\n");
      }
      else {
        this.baselineDate = date;
      }
    }
    start =
        generateTimestamp(this.baselineDate, "T00:00:00.000", TimeZone.getDefault())
            .toGregorianCalendar();
    currentRequestTime = start;
    // This is the baseline data request loop.
    for (int i = 0; i < HOURS_IN_DAY; i++) {
      double value;
      // If clone() is not used, altering this Calendar also alters the other Calendar.
      Calendar oneHourAhead = (Calendar) currentRequestTime.clone();
      oneHourAhead.add(Calendar.HOUR_OF_DAY, 1);
      // Temporarily convert calendars to XMLGregorianCalendars.
      XMLGregorianCalendar bound1 =
          generateTimestamp(DATE_FORMAT.format(currentRequestTime.getTime()),
              TIME_FORMAT.format(currentRequestTime.getTime()), TimeZone.getDefault());
      XMLGregorianCalendar bound2 =
          generateTimestamp(DATE_FORMAT.format(oneHourAhead.getTime()),
              TIME_FORMAT.format(oneHourAhead.getTime()), TimeZone.getDefault());
      // Format and store the energy consumption value.
      value =
          Double.parseDouble(VALUE_FORMAT.format(client.getEnergyConsumed(source.getName(), bound1,
              bound2, 0) / 1000));
      energyConsumption[i] = value;
      // Move up the starting time for date requests. As before, use clone() to avoid
      // altering the original calendar.
      currentRequestTime = (Calendar) oneHourAhead.clone();
    }
    this.baselineValues = energyConsumption;
    this.sourceName = source.getName();
  }

  /**
   * Generates the GMT offset in hours, returned as a string in the form +hh:mm or -hh:mm.
   * 
   * @param zone A time zone.
   * @return The GMT offset of the specified time zone in the form +hh:mm or -hh:mm, where h is
   * hours and m is minutes.
   */
  private String gmtOffsetString(TimeZone zone) {
    String result = "";
    String minutesOffset = ":00";
    // Convert to hours: 1000 milliseconds/second * 60 seconds/minute * 60 minutes/hour = 1 hour.
    int offsetHours = zone.getRawOffset() / (1000 * 60 * 60);
    String offsetString = Integer.toString(offsetHours);
    if (Math.abs(offsetHours) < 10) {
      if (offsetHours < 0) {
        // e.g., "-8" becomes "-08:00"
        result =
            offsetString.substring(0, 1) + "0"
                + offsetString.substring(1, Integer.toString(offsetHours).length()) + minutesOffset;
      }
      else {
        // e.g., "7" becomes "+07:00"
        result = "+0" + offsetString + minutesOffset;
      }
    }
    else {
      if (offsetHours >= 0) {
        // e.g., "10" becomes "+10:00"
        result = "+" + offsetHours + minutesOffset;
      }
      else if (offsetHours < 0) {
        // e.g., "-10" becomes "-10:00"
        result = offsetHours + minutesOffset;
      }
    }
    return result;
  }

  /**
   * Returns an XMLGregorianCalendar with its time set to the parameter of "time". This function
   * does not perform any input range checking.
   * 
   * @param date The date, in format yyyy-mm-dd.
   * @param time The time, in format THH:mm:ss.SSS.
   * @param zone The user's specified time zone.
   * @return An XMLGregorianCalendar representing the desired timestamp.
   * @throws Exception If a method call within the method throws an exception.
   */
  private XMLGregorianCalendar generateTimestamp(String date, String time, TimeZone zone)
      throws Exception {
    return Tstamp.makeTimestamp(date + time + gmtOffsetString(zone));
  }

  /**
   * A utility method that returns a string (yyyy-MM-dd) representing the date before today.
   * 
   * @param calendar A Calendar object representing today's date.
   * @return A string in the form yyyy-MM-dd representing the date yesterday.
   */
  private String generateYesterday(Calendar calendar) {
    // Roll back the date.
    calendar.add(Calendar.DATE, -1);
    int year = calendar.get(Calendar.YEAR);
    // Account for Calendar months starting at 0 instead of 1.
    int month = calendar.get(Calendar.MONTH) + 1;
    int day = calendar.get(Calendar.DAY_OF_MONTH);
    String monthString;
    String dayString;
    String result;

    if (month < 10) {
      monthString = "0" + month;
    }
    else {
      monthString = Integer.toString(month);
    }
    if (day < 10) {
      dayString = "0" + day;
    }
    else {
      dayString = Integer.toString(day);
    }
    result = year + "-" + monthString + "-" + dayString;
    return result;
  }
}
